package gov.nysenate.openleg.controller.api.admin;
import com.google.common.collect.ImmutableMap;
import gov.nysenate.openleg.client.response.base.BaseResponse;
import gov.nysenate.openleg.client.response.base.ListViewResponse;
import gov.nysenate.openleg.client.response.base.SimpleResponse;
import gov.nysenate.openleg.client.response.base.ViewObjectResponse;
import gov.nysenate.openleg.client.response.error.ErrorCode;
import gov.nysenate.openleg.client.response.error.ErrorResponse;
import gov.nysenate.openleg.client.response.error.ViewObjectErrorResponse;
import gov.nysenate.openleg.client.view.base.ListView;
import gov.nysenate.openleg.client.view.entity.AdminUserView;
import gov.nysenate.openleg.config.Environment;
import gov.nysenate.openleg.controller.api.base.BaseCtrl;
import gov.nysenate.openleg.controller.api.base.InvalidRequestParamEx;
import gov.nysenate.openleg.model.auth.AdminUser;
import gov.nysenate.openleg.service.auth.AdminUserService;
import gov.nysenate.openleg.service.auth.InvalidUsernameException;
import gov.nysenate.openleg.service.auth.OpenLegRole;
import gov.nysenate.openleg.service.mail.SendMailService;
import gov.nysenate.openleg.util.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.annotation.RequiresUser;
import org.mindrot.jbcrypt.BCrypt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.*;
import java.util.stream.Collectors;
import static gov.nysenate.openleg.controller.api.base.BaseCtrl.BASE_ADMIN_API_PATH;
@RestController
@RequestMapping(value = BASE_ADMIN_API_PATH + "/accounts")
public class AdminAccountCtrl extends BaseCtrl
{
private static final Logger logger = LoggerFactory.getLogger(AdminAccountCtrl.class);
@Autowired
private AdminUserService adminUserService;
@Autowired
private SendMailService sendMailService;
@Autowired
Environment environment;
private static final String registrationEmailSubject = "OpenLegislation admin registration";
private static final String registrationEmailTemplate =
"Hello,\n\n" +
"\tYou are receiving this email because you have been registered as an administrative user of OpenLegislation. " +
"Your login credentials are as follows:\n\n\tusername: ${username}\n\tpassword: ${password}\n\n" +
"Log in at ${base_url}/admin/account to access your account";
private static final int newPassLength = 8;
private static final int minPassLength = 5;
@RequiresUser
@RequestMapping(value = "/logout", method = RequestMethod.GET)
@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public BaseResponse logout() {
SecurityUtils.getSubject().logout();
return new SimpleResponse(true, "you have been logged out", "logout");
}
@RequiresPermissions("admin:account:view")
@RequestMapping(value = "", method = RequestMethod.GET)
public BaseResponse getAdminUsers() {
return new ViewObjectResponse<>(ListView.of(
adminUserService.getAdminUsers().stream()
.map(AdminUserView::new)
.collect(Collectors.toList())));
}
@RequiresPermissions("admin:account:view")
@RequestMapping(value = "/{username:.+}", method = RequestMethod.GET)
public BaseResponse getAdminUser(@PathVariable String username) {
if (StringUtils.isBlank(username)) {
return getAdminUsers();
}
if (!adminUserService.adminInDb(username)) {
throw new UserNotFoundException(username);
}
return new ViewObjectResponse<>(new AdminUserView(adminUserService.getAdminUser(username)));
}
/**
* Create New Admin User API
* -------------------------
*
* Registers a new user as an admin. Sends an email message confirming the registration along with a new,
* randomly generated password. Must be called by a master admin.
*
* (GET) /api/3/admin/accounts/new
*
* Request params: username (string) - The username of the new user, should be an approved email address
* master (boolean) - True if the new user should be a master admin (default false)
*
* Expected Output: successful admin-registered response if the user was created, ErrorResponse otherwise
*/
@RequiresPermissions("admin:account:modify")
@RequestMapping(value = "/{username:.+}", method = RequestMethod.POST)
public Object createNewUser(@PathVariable String username,
@RequestParam(defaultValue = "false") boolean master) {
if (adminUserService.adminInDb(username)) {
return new ResponseEntity<>(
new ViewObjectErrorResponse(ErrorCode.USER_ALREADY_EXISTS, username), HttpStatus.CONFLICT);
}
String password = RandomUtils.getRandomString(newPassLength);
try {
adminUserService.createUser(username, password, true, master);
} catch (InvalidUsernameException ex) {
throw new InvalidRequestParamEx(username, "username", "String", ex.getProperFormat());
}
sendNewUserEmail(username, password);
return new SimpleResponse(true, username + " has been successfully registered as an admin user", "admin-registered");
}
/**
* Remove Admin User API
* ---------------------
*
* Deletes the account of an admin user. Must be called by a master admin.
*
* (GET) /api/3/admin/accounts/remove
*
* Request params: username (string) - The username of the user to be removed
*
* Expected Output: successful admin-deleted if a user was removed, ErrorResponse otherwise
*/
@RequiresPermissions("admin:account:modify")
@RequestMapping(value = "/{username:.+}", method = RequestMethod.DELETE)
public Object removeUser(@PathVariable String username) {
if (!adminUserService.adminInDb(username)) {
throw new UserNotFoundException(username);
}
if (environment.getDefaultAdminName().equals(username)) {
return new ResponseEntity<>(
new ViewObjectErrorResponse(ErrorCode.CANNOT_DELETE_ADMIN, username), HttpStatus.FORBIDDEN);
}
adminUserService.deleteUser(username);
return new SimpleResponse(true, "The admin user " + username + " has been successfully removed", "admin-deleted");
}
/**
* Change Password API
* -------------------
*
* Changes the password for the calling user.
*
* (POST) /api/3/admin/accounts/passchange
*
* Request params: password (string) - The new password
*
* Expected Output: successful pass-changed response if the password was changed, ErrorResponse otherwise
*/
@RequiresPermissions("admin")
@RequestMapping(value = "/passchange", method = RequestMethod.POST)
public Object changePassword(@RequestParam(required = true) String password) {
String username = getSubjectUsername();
AdminUser user = adminUserService.getAdminUser(username);
if (BCrypt.checkpw(password, user.getPassword())) {
return new ResponseEntity<>(
new ErrorResponse(ErrorCode.SAME_PASSWORD), HttpStatus.BAD_REQUEST);
}
if (password.length() < minPassLength) {
throw new InvalidRequestParamEx(password.replaceAll(".", "*"), "password", "String",
"Password must contain at least " + minPassLength + " characters");
}
user.setPassword(password);
adminUserService.createUser(user);
return new SimpleResponse(true, "Password has been successfully changed", "pass-changed");
}
/** --- Exception Handlers --- */
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public BaseResponse handleUserNotFoundException(UserNotFoundException ex) {
return new ViewObjectErrorResponse(ErrorCode.USER_DOES_NOT_EXIST, ex.getUsername());
}
/**
* --- Internal Methods ---
*/
/**
* @return The username of the current subject
*/
private String getSubjectUsername() {
return SecurityUtils.getSubject().getPrincipal().toString();
}
/**
* Sends an email to a new user notifying them of their registration
* @param username The username/email address of the new user
* @param password the password of the new user
*/
@Async
private void sendNewUserEmail(String username, String password) {
String message = StrSubstitutor.replace(registrationEmailTemplate,
ImmutableMap.of("username", username, "password", password, "base_url", environment.getUrl()));
sendMailService.sendMessage(username, registrationEmailSubject, message);
}
private class UserException extends RuntimeException
{
private static final long serialVersionUID = -6422565299854256546L;
private String username;
public UserException(String message, String username) {
super(message);
this.username = username;
}
public String getUsername() {
return username;
}
}
private class UserNotFoundException extends UserException {
private static final long serialVersionUID = 3276041543957882445L;
public UserNotFoundException(String username) {
super("User " + username + " was not found!", username);
}
}
}